﻿using System;
using System.Collections.Generic;
using System.Text;
using GeoRef;
using Storage;
using Storage.Exceptions;
using System.Web.Security;
using System.Linq;
using System.Data.SqlTypes;
using Security;

namespace Groupware
{
    /// <summary>
    /// This class represent a trip reservation request addressed to the trip owner
    /// </summary>
    public class ReservationRequest
    {
        #region Constants

        internal enum State : byte
        {
            PENDING = 0,
            REFUSED = 1,
            DELETED = 2
        };

        #endregion

        #region private Fields

        private int id = -1;
        private int userId;
        private String username;
        private GeoLoc start;
        private GeoLoc end;
        private float startTolerance;
        private float endTolerance;
        private byte seatsNo;
        private String notes;
        private DateTime date;
        private byte status = (byte)State.PENDING;

        #endregion

        #region Constructors

        internal ReservationRequest() { }

        /// <summary>
        /// Create a reservation request from the corrispondent database entity
        /// </summary>
        /// <param name="rr">the db reservation request object</param>
        internal ReservationRequest(Storage.EntityModels.ReservationRequest rr)
        {
            Update(rr);
        }

        internal void Update(Storage.EntityModels.ReservationRequest rr)
        {
            Storage.EntityModels.Place s = rr.OriginPlace, e = rr.DestinationPlace;
            userId = rr.UserId;
            username = rr.User.Username;
            start = new GeoLoc(s);
            end = new GeoLoc(e);
            startTolerance = rr.StartTolerance;
            endTolerance = rr.EndTolerance;
            seatsNo = rr.RequestedSeats;
            notes = rr.Notes;
            date = rr.Date;
            status = rr.Status;
            id = rr.Id;
        }

        #endregion

        #region Properties

        /// <summary>
        /// The requester user id
        /// </summary>
        public int UserId
        {
            get { return userId; }
            internal set { userId = value; }
        }

        /// <summary>
        /// The requester username
        /// </summary>
        public String Username
        {
            get { return username; }
            internal set { username = value; }
        }

        /// <summary>
        /// The location the requesting user would like to be taken on
        /// </summary>
        public GeoLoc Start
        {
            get { return start; }
            internal set { start = value; }
        }

        /// <summary>
        /// The location the requesting user would like to be dropped
        /// </summary>
        public GeoLoc End
        {
            get { return end; }
            internal set { end = value; }
        }

        /// <summary>
        /// The tolerance radius for which the requester is agreeable to move the start location
        /// (expressed in kilometers)
        /// </summary>
        public float StartTolerance
        {
            get { return startTolerance; }
            internal set { startTolerance = value; }
        }

        /// <summary>
        /// The tolerance radius for which the requester is agreeable to move the end location
        /// (expressed in kilometers)
        /// </summary>
        public float EndTolerance
        {
            get { return endTolerance; }
            internal set { endTolerance = value; }
        }

        /// <summary>
        /// The number of seats the requesting user wants to reserve
        /// </summary>
        public byte SeatsNo
        {
            get { return seatsNo; }
            internal set { seatsNo = value; }
        }

        /// <summary>
        /// The trip which the reservation request is made for
        /// </summary>
        /// <remarks>every time you get this property a new Trip object is created</remarks>
        public Trip Trip
        {
            get 
            {
                Storage.EntityModels.ReservationRequest rr = GetDbReservationRequest();
                return rr == null ? null : new Trip(rr.Trip);
            }
        }

        /// <summary>
        /// General notes addressed to the trip owner (if the request is pending)
        /// or
        /// Refuse motivation provided by the trip owner (if the request is refused)
        /// </summary>
        public String Notes
        {
            get { return notes; }
            internal set { notes = value; }
        }

        /// <summary>
        /// The date the reservation request was done (if pending)
        /// otherwise the date it was refused
        /// </summary>
        public DateTime Date
        {
            get { return date; }
            internal set { date = value; }
        }

        #endregion

        #region Public methods

        /// <returns>true if the reservation request is still pending, false otherwise</returns>
        public bool IsPending()
        {
            return status == (byte)State.PENDING;
        }

        /// <returns>true if the reservation request has been refused by the trip owner, false otherwise</returns>
        public bool IsRefused()
        {
            return status == (byte)State.REFUSED;
        }

        /// <returns>true if the reservation request has been deleted, false otherwise</returns>
        public bool IsDeleted()
        {
            return status == (byte)State.DELETED;
        }

        /// <summary>
        /// Refuse this reservation request
        /// </summary>
        /// <param name="owner">the owner of the trip this request is made for</param>
        /// <param name="motivation">the refuse motivation</param>
        /// <returns>true if operation succeded, false otherwise</returns>
        /// <exception cref="OperationNotAllowedException">if operation isn't performed by the owner of the trip this request is made for</exception>
        public bool Refuse(TripLoaUser owner, String motivation)
        {
            Storage.EntityModels.ReservationRequest rr = GetDbReservationRequest();
            if (rr == null)
                return false;
            Update(rr);
            if (rr.Trip.OwnerId != owner.UserID)
                throw new OperationNotAllowedException("Only the trip owner can refuse the reservation request.");
            if (IsRefused())
                return false;
            rr.Date = this.date = DateTime.Now;
            rr.Status = this.status = (byte)State.REFUSED;
            rr.Notes = this.notes = motivation;
            StorageManager.Instance.Entities.SubmitChanges();

            Notification refuseNotification = new Notification()
            {
                TemplateType = Notification.DENIED_RESERVATION,
                TripId = rr.Trip.Id,
                SenderID = owner.UserID
            };
            if (!string.IsNullOrEmpty(motivation)) refuseNotification.AddField("motivation", motivation);
            else refuseNotification.AddField("motivation", "");
            refuseNotification.Store(rr.UserId);

            return true;
        }

        /// <summary>
        /// Delete this reservation request.
        /// This method can be only used by the user who submitted this request.
        /// </summary>
        /// <param name="user">the user who submitted the request</param>
        /// <returns>true if operation succeded, false otherwise</returns>
        /// <exception cref="OperationNotAllowedException">if operation isn't performed by the user who submitted this request</exception>
        public bool Delete(TripLoaUser user)
        {
            if (user.UserID != userId)
                throw new OperationNotAllowedException("Only the user who submitted this request can delete it");
            Storage.EntityModels.ReservationRequest rr = GetDbReservationRequest();
            if (rr == null)
                return false;
            Update(rr);
            this.status = (byte)State.DELETED;
            using (Storage.EntityModels.TripLoaDataContext db = StorageManager.Instance.Entities)
            {
                db.ReservationRequests.DeleteOnSubmit(rr);
                //DELETE ASSOCIATED PLACES ...
                db.SubmitChanges();
            }

            Notification passengerDelete = new Notification()
            {
                TemplateType = Notification.PASSENGER_DELETE,
                TripId = rr.Trip.Id,
                SenderID = user.UserID
            };
            passengerDelete.Store((int)rr.Trip.OwnerId);

            return true;
        }

        #endregion

        #region Internal methods
        // WARNING: methods performing db operations do NOT call SubmitChanges()

        /// <summary>
        /// Gets the corrispondent database entity for this reservation request 
        /// </summary>
        /// <returns>the db entity if exist, null otherwise</returns>
        private Storage.EntityModels.ReservationRequest GetDbReservationRequest()
        {
            if (status == (byte)State.DELETED || id <= 0)
                return null;

            var rr = from Storage.EntityModels.ReservationRequest r in StorageManager.Instance.Entities.ReservationRequests
                     where r.Id == id
                     select r;
            if (rr.Count() == 0)
            {
                status = (byte)State.DELETED;
                return null;
            }
            return rr.First();
        }

        /// <summary>
        /// Validate a new Reservation Request in order to submit it 
        /// </summary>
        /// <param name="trip">The trip this request is to be check against</param>
        /// <returns>null if validation succeded, otherwise a Dictionary  containing [parameterName,Exception] pairs</returns>
        internal IDictionary<String, Exception> Validate(Trip trip)
        {
            Dictionary<String, Exception> errors = new Dictionary<string, Exception>();
            //Error checks on object fields
            if (start == null)
                errors.Add("start", new ArgumentNullException("start"));
            else if (!trip.CheckDistance(start))
                errors.Add("start", new InvalidStartPointException("Start point too far from trip path"));
            if (end == null)
                errors.Add("end", new ArgumentNullException("end"));
            else if (!trip.CheckDistance(end))
                errors.Add("end", new InvalidEndPointException("End point too far from trip path"));
            if (seatsNo <= 0)
                errors.Add("seatsNo", new ArgumentOutOfRangeException("seatsNo","Reservation allows at least 1 seat"));
            if (startTolerance < 0)
                errors.Add("startTolerance", new ArgumentOutOfRangeException("startTolerance", "Negative tolerance for start location"));
            if (endTolerance < 0)
                errors.Add("endTolerance", new ArgumentOutOfRangeException("endTolerance", "Negative tolerance for end location"));
            return errors.Count == 0 ? null : errors;
        }

        /// <summary>
        /// Inserts into the db this reservation request as pending
        /// </summary>
        internal void AddToDB(Trip trip)
        {
            Storage.EntityModels.Place start, end;
            //Saving start and end Places
            start = Start.SavePlace();
            end = End.SavePlace();
            //DB Insertion
            id = StorageManager.Instance.AddReservationRequest(trip.Id, username, seatsNo, start, end, startTolerance, endTolerance, notes, (byte)State.PENDING);
        }

        /// <summary>
        /// Delete this reservation request from database
        /// </summary>
        internal void DeleteFromDB()
        {
            Storage.EntityModels.ReservationRequest rr = GetDbReservationRequest();
            if (rr != null)
            {
                StorageManager.Instance.Entities.ReservationRequests.DeleteOnSubmit(rr);
                //DELETE ASSOCIATED PLACES ...
            }
        }

        /// <summary>
        /// Checks whether this reservation request still exists on database
        /// </summary>
        /// <returns>true if the corresponding db entity exists, false otherwise</returns>
        internal bool Exists()
        {
            if (status == (byte)State.DELETED) return false;
            return (GetDbReservationRequest() != null);
        }

        #endregion

        #region Check Distance Methods
        
        /// <summary>
        /// Checks whether a location respects the tolerance for the start location
        /// </summary>
        /// <param name="choosenLocation">the location to check</param>
        /// <returns>true if check passed, false otherwise</returns>
        internal bool CheckStartDistance(GeoLoc choosenLocation)
        {
            return this.start.Distance(choosenLocation) <= this.startTolerance;
        }

        /// <summary>
        /// Checks whether a location respects the tolerance for the end location
        /// </summary>
        /// <param name="choosenLocation">the location to check</param>
        /// <returns>true if check passed, false otherwise</returns>
        internal bool CheckEndDistance(GeoLoc choosenLocation)
        {
            return this.end.Distance(choosenLocation) <= this.endTolerance;
        }

        #endregion

        #region Static Methods

        /// <summary>
        /// Gets all pending reservation requests submitted by a specified user.
        /// </summary>
        /// <param name="user">the user</param>
        /// <returns>the pending Reservation Request list</returns>
        public static List<ReservationRequest> GetPendingReservationRequestsByUser(TripLoaUser user)
        {
            if (user == null)
                throw new ArgumentNullException("user");
            // Gets all db entities ...
            var q = from rr in StorageManager.Instance.Entities.ReservationRequests
                    where rr.UserId == user.UserID && rr.Status == (byte)State.PENDING
                    select rr;
            /// ... and converts them to a list of high-level objects
            List<ReservationRequest> list = q.ToList().ConvertAll(r => new ReservationRequest(r));            
            return list;
        }

        /// <summary>
        /// Gets all refused reservation requests submitted by a specified user.
        /// </summary>
        /// <param name="user">the user</param>
        /// <returns>the refused Reservation Request list</returns>
        public static List<ReservationRequest> GetRefusedReservationRequestsByUser(TripLoaUser user)
        {
            if (user == null)
                throw new ArgumentNullException("user");
            // Gets all db entities ...
            var q = from rr in StorageManager.Instance.Entities.ReservationRequests
                    where rr.UserId == user.UserID && rr.Status == (byte)State.REFUSED
                    select rr;
            /// ... and converts them to a list of high-level objects
            List<ReservationRequest> list = q.ToList().ConvertAll(r => new ReservationRequest(r));
            return list;
        }

        #endregion

        #region Exceptions

        [Serializable]
        public class InvalidStartPointException : Exception
        {
            public InvalidStartPointException(string message) : base(message) { }
        }

        [Serializable]
        public class InvalidEndPointException : Exception
        {
            public InvalidEndPointException(string message) : base(message) { }
        }

        #endregion
    }
}
